目前TanStack有这些内容,我难道都要学习吗?很明显不是。

以下是截至 2025年12月 TanStack 生态中这些库的主要作用和当前成熟度简要说明(基于官方文档和最新状态):
| 库名称 | 成熟度 | 主要作用 / 解决什么问题 | 典型使用场景 | 备注 / 推荐程度 |
|---|---|---|---|---|
| TanStack Start | RC | 全栈 React 应用框架(基于 Router + Vite),提供类型安全的 SSR、Streaming、Server Functions 等 | Next.js / Remix 替代方案,全栈 SPA/SSR 项目 | 正在冲 1.0,适合尝鲜全栈 |
| TanStack Router | RC → 稳定 | 最强大、最类型安全的 React 路由器,支持文件路由、数据加载、搜索参数、上下文等 | 中大型 SPA、全栈应用路由核心 | 目前 TanStack 生态最成熟产品之一 |
| TanStack Query | 稳定 | 服务端状态管理 / 数据获取库(原 React Query),缓存、自动重试、乐观更新、无限滚动等 | API 数据获取、缓存、实时同步 | 几乎所有 React 项目必备 |
| TanStack Table | BETA → 稳定 | 功能强大的表组件(原 React Table),支持排序、分页、过滤、虚拟化、列拖拽等 | 数据表格、仪表盘、复杂列表 | v8+ 版本非常强大,广泛使用 |
| TanStack DB | BETA | 响应式客户端数据库 + 集合 + 实时查询 + 乐观更新,构建在 differential dataflow 上 | 本地优先/实时应用、大数据集即时交互 | 2025年新星,适合追求极致性能 |
| TanStack AI | ALPHA | 开源 AI SDK,提供统一接口支持多个 AI 提供商(无厂商锁定) | 集成 ChatGPT/Claude/Gemini 等 AI 服务 | 早期阶段,关注 TypeScript 友好 |
| TanStack Form | NEW | 轻量、类型安全、框架无关的表单库,支持 Zod/Yup 等校验 | 复杂表单、动态表单、多步表单 | 较新,定位取代 Formik/React Hook Form |
| TanStack Virtual | 稳定 | 头文件虚拟化库(原 react-virtual),高效渲染超长列表/表格/网格(60fps) | 大数据列表、聊天记录、虚拟滚动表格 | 非常成熟,性能极强 |
| TanStack Pacer | BETA | 性能优化原语集合:防抖、节流、限流、队列、批处理等 | 输入优化、API 限流、批量更新 | 实用工具集,解决常见性能痛点 |
| TanStack Store | ALPHA | 不可变 + 响应式数据存储,为 TanStack 生态核心提供动力(类似微型 zustand) | 全局/局部状态管理 | 内部使用较多,外部项目较少 |
| TanStack Devtools | ALPHA | 统一的 TanStack 库开发工具面板(Query/Router/Table 等 devtools 集合) | 调试 TanStack 生态所有库 | 正在统一,未来会很强大 |
核心稳定三件套:Query + Router + Table / Virtual ,项目中用好这三个就行了。如果Start稳定了,那么就都转到Start上面去即可。
按照shadcn创建项目的步骤来创建即可:https://ui.shadcn.com/docs/installation/vite。
安装依赖:pnpm add @tanstack/react-form
dev工具:
xxxxxxxxxx21pnpm add @tanstack/react-devtools2pnpm add @tanstack/react-form-devtools在main.tsx里面使用devtools。

启动项目,出现dev图标即可:

在 tanstack/React Form 中,核心是通过 useForm 创建表单实例,然后使用 form.Field 来管理具体字段。
useForm 钩子:表单的大脑这是整个表单的控制中心。
数据初始化:通过 defaultValues 定义表单的初始状态。由于 TanStack Form 是全类型安全的,它会根据这个对象自动推导出整个表单的类型。
逻辑封装:它管理着表单的提交状态、校验逻辑和全局状态(如 isSubmitting、canSubmit 等)。
配置方式:
xxxxxxxxxx41const form = useForm({2 defaultValues: { firstName: '', lastName: '' },3 onSubmit: async ({ value }) => { }, // 统一处理提交逻辑4})form.Field 组件:细粒度订阅 (Fine-grained Subscriptions)这是 TanStack Form 性能强大的核心原因。
form.Field 是一个单独的组件。当 firstName 改变时,只有对应的这个 Field 组件会重新渲染,页面的其他部分(如 lastName 或表单标题)完全不会受到影响。field 实例暴露出来,让你完全控制 UI。field 实例提供的核心属性在 form.Field 内部,通过参数拿到的 field 对象包含了管理该字段所需的一切:
状态管理 (field.state.value): 受控组件模式。你将 field.state.value 绑定到 input 的 value 上,确保数据流向清晰。
事件处理器 (handleChange / handleBlur):
handleChange:不仅更新值,还会触发该字段定义的校验逻辑。handleBlur:标记该字段为“已触碰 (touched)”,通常用于在用户离开输入框后再显示错误信息,提升体验。元数据 (field.state.meta): 包含了字段的衍生状态,如 errors(错误信息数组)、isTouched(是否被访问过)、isPristine(是否未动过)。
示例中展示了标准的 HTML 提交拦截方式:
xxxxxxxxxx71<form2 onSubmit={(e) => {3 e.preventDefault() // 阻止原生刷新4 e.stopPropagation()5 form.handleSubmit() // 调用 useForm 定义的 onSubmit6 }}7>这种方式将 DOM 事件与表单逻辑完全解耦。
xxxxxxxxxx851// src/lib/RegisterForm.tsx23import { formOptions, useForm } from "@tanstack/react-form";45// 定义表单的ts类型6interface RegisterForm {7 firstName: string;8 lastName: string;9 age: number | undefined;10 email: string;11 password: string;12 confirmPassword: string;13 address: {14 street: string;15 city: string;16 state: string;17 zipCode: string;18 };19 skills: {20 name: string;21 id: string;22 level: "beginner" | "intermediate" | "expert";23 }[];24 acceptTerms: boolean;25}2627// 设置默认值28const defaultValues: RegisterForm = {29 firstName: "",30 lastName: "",31 age: undefined,32 email: "",33 password: "",34 confirmPassword: "",35 address: {36 street: "",37 city: "",38 state: "",39 zipCode: "",40 },41 skills: [],42 acceptTerms: false,43};4445// 定义 useForm() 的参数46const formOpts = formOptions({47 defaultValues,48});4950export default function RegisterForm() {51 const form = useForm({52 formOpts,53 onSubmit: (data) => {54 console.log({ data });55 },56 });5758 return (59 <div className="text-start max-w-100 p-4">60 <form61 onSubmit={(e) => {62 e.preventDefault();63 form.handleSubmit();64 }}>65 <form.Field name="firstName">66 {(field) => {67 return (68 <div className="mb-4">69 <input70 type="text"71 placeholder="First Name"72 className="border border-gray-600 p-2 w-full"73 value={field.state.value}74 onChange={(e) => field.handleChange(e.target.value)}75 onBlur={field.handleBlur}76 />77 </div>78 );79 }}80 </form.Field>81 <button type="submit">Submit</button>82 </form>83 </div>84 );85}在App.tsx里面引入这个form,查看效果,可以看到,submit的data里面,数据是正常的,打开devtools,可以看到各种表单状态。

注意:tanstack/react-form的devtools还没有成熟,有点bug,如果看不到表单,要么刷新页面,要么重启服务。
看上去@tanstack/react-form有点类似react hook form,确实有很多地方相同。但还是很不同的:
特性 @tanstack/react-form React Hook Form 状态管理 受控模式 (Controlled) 但高性能 非受控模式 (Uncontrolled) 为主 类型安全 极强。深度嵌套对象的类型推导非常完美 较强,但深度嵌套时类型有时会失效 学习曲线 略高(函数式响应式思维) 较低(更符合 HTML 标准) 校验器支持 内置并完美支持 Zod, Valibot, Yup 等 需配合 Resolver 框架支持 React, Vue, Svelte, Solid, Angular 主要是 React
在 @tanstack/react-form 中,Field Meta Data(字段元数据) 是指除了字段本身的“值”之外,描述该字段当前状态的一组信息。
简单来说,如果 value 是用户输入的内容,那么 meta 就是关于这个内容及其输入过程的“体检报告”。
在一个复杂的表单中,你不仅需要知道用户填了什么,还需要知道:
当你通过 field.state.meta 访问时,最常用的属性包括:
| 属性 | 类型 | 含义 | 典型用途 |
|---|---|---|---|
isTouched | boolean | 字段是否被聚焦并失焦过(Blur)。 | 错误提示:通常只有在 isTouched 为 true 时才显示报错,防止用户还没开始填就满屏红字。 |
isDirty | boolean | 当前值是否与初始值(defaultValues)不同。 | 状态监控:用来判断表单是否被修改,决定是否弹出“离开页面不保存”的提醒。 |
errors | Array | 包含该字段所有校验失败信息的数组。 | UI 展示:遍历此数组显示具体的错误文字。 |
isValidating | boolean | 异步校验是否正在进行中。 | 加载反馈:如果设置了远程校验(如检查用户名是否重复),可用它显示微小的 Spinner。 |
isPristine | boolean | 字段是否从未被修改过。 | 与 isDirty 相反。 |
通过 form.useStore((s) => s.meta) 或<form.Subscribe />。常用属性:
isSubmitting: 整个表单是否正在提交过程中。常用于禁用提交按钮并显示 Spinner。
isValid: 表单内所有字段是否都通过了校验。
isDirty: 表单中是否有任何一个字段被修改过。用于控制“重置”按钮或离开页面提醒。
isTouched: 表单中是否有任何一个字段被触碰过。
canSubmit: 一个综合状态,通常表示 !isSubmitting && isValid。
通过你在上一条信息中提到的 field 对象,你可以非常灵活地控制 UI 的展现逻辑。
xxxxxxxxxx231<form.Field2 name="email"3 children={(field) => (4 <div>5 <input6 value={field.state.value}7 onBlur={field.handleBlur} // 触发 isTouched 状态8 onChange={(e) => field.handleChange(e.target.value)}9 className={field.state.meta.errors.length ? 'input-error' : ''}10 />11 12 {/* 逻辑:只有当用户点过该输入框 (isTouched) 且 有错误时,才显示文字 */}13 {field.state.meta.isTouched && field.state.meta.errors.length ? (14 <span style={{ color: 'red' }}>15 {field.state.meta.errors[0]}16 </span>17 ) : null}1819 {/* 逻辑:如果是异步校验,显示提示 */}20 {field.state.meta.isValidating ? <span>检查中...</span> : null}21 </div>22 )}23/>isTouched = true。当你调用 field.handleBlur() 时,TanStack Form 会自动更新 Meta 数据。meta.errors 里的内容会与你的校验逻辑完美对应。meta 状态改变时(比如 isValidating 从 true 变 false),依然只有当前的 Field 会重新渲染,性能极佳。Meta Data 是表单的“上下文信息”。没有它,你只能做一个能提交的表单;有了它,你才能做一个拥有完美用户体验(例如:按需报错、提交禁用、异步加载反馈)的高级表单。
当field变化时,devtools里面可以看到meta相关属性都变化了。而且form相关的meta也变化了。

在 UI 开发(尤其是 React 和 TanStack 系列)中,Reactivity(响应式) 是指一种“数据驱动 UI 自动更新”的机制。
简单来说:当数据(State)发生变化时,所有依赖该数据的界面部分(UI)应当能够“自动”且“高效”地同步更新,而不需要你手动去操作 DOM。
听上去类似于vue的数据双向绑定,但二者是有区别的,最显著的区别就是tanstack form里面需要显式的订阅来实现响应式。
谈到 Reactivity(响应式),Vue 是这个领域的“鼻祖”级代表。虽然 TanStack Form (React) 和 Vue 都追求“数据变了 UI 跟着变”,但它们的底层实现逻辑和开发者的心智模型有本质区别。
1. 实现机制的差异:Proxy vs. Hooks
Vue: 自动追踪 (Mutable & Transparent)
- 原理:Vue 使用 JavaScript 的
Proxy拦截对象。当你修改form.value = 'new'时,Vue 已经自动知道了谁在用这个数据,并直接触发更新。- 感受:非常直观,就像操作普通 JS 对象一样,不需要显式调用
set方法。TanStack Form (React): 显式订阅 (Immutable & Hook-based)
- 原理:React 本身没有响应式,它只有“状态驱动重绘”。TanStack Form 在 React 外部维护了一个状态存储(Store),然后通过
useStore或form.Field这种 Hook 机制,显式地订阅某些数据的变化。- 感受:你需要通过
field.handleChange(value)这种特定的 API 来更新,而不是直接赋值。2. 渲染精度的差异:组件级 vs. 节点级
Vue:
Vue 的响应式是“组件级”的。如果一个组件里用了 form.name,当 name 变了,Vue 会重新运行这个组件的渲染函数。Vue 通过虚拟 DOM 优化,性能非常高。
TanStack Form (React 版):
它在 React 内部实现了更细粒度的控制。由于它使用了订阅模式,你可以做到:虽然你在一个超大的 MyForm 组件里定义了表单,但当 firstName 改变时,整个 MyForm 组件不会重新渲染,只有被 <form.Field name="firstName"> 包裹的那一小块 UI 会动。
3. 心智模型:谁更“黑盒”?
特性 Vue (Reactivity) TanStack Form (React) 透明度 高。你看不见订阅过程,Vue 帮你做了。 低。你需要理解“订阅”和“选择器(Selector)”。 依赖声明 自动。你在模板里用了,它就自动收集依赖。 手动。你需要通过 name="xxx"或selector声明。可预测性 极佳。但有时候难以追踪是谁改了数据(特别是嵌套深时)。 极佳。所有状态流向都是显式的,Debugger 好追溯。 4. 为什么 TanStack Form 在 React 里显得很像 Vue?
如果你觉得 TanStack Form 用起来有种“Vue 感”,那是因为它在努力克服 React 的缺点:
- React 默认是“牵一发而动全身”(父组件变了子组件跟着变)。
- TanStack Form 通过内部的
Store避开了 React 的全量渲染。- 它让 React 开发者也能享受到 Vue 那种“改个字段,只跳动那个字段”的局部响应式快感。
5. 总结
- Vue 是将响应式做进了语言的血液里,整个框架就是为此设计的。
- TanStack Form 是在 React 的机制上,外挂了一个高性能的响应式引擎。
如果你习惯 Vue,你会觉得 TanStack Form 的
field.state非常亲切;如果你习惯 React,你会觉得 TanStack Form 解决了 React 处理大表单时的性能噩梦。
在没有响应式机制的年代(比如原生 JS 或早期 jQuery),更新界面是极其繁琐的:
document.getElementById(...).innerText = newValue。响应式机制解决了上述痛点,带来了以下三个核心优势:
你只需要描述 UI “长什么样”(基于当前数据),而不需要告诉程序 “怎么去做”(更新步骤)。
isError 变量。当 isError 变为 true,UI 自动变红。这是 TanStack 系列最引以为傲的地方。优秀的响应式系统能精准定位到“哪一个组件、哪一行文字”需要变,而不是暴力地刷新整个页面。
数据只存在一个地方,所有引用它的组件都会同步变化。这保证了复杂应用中状态的绝对一致性。
TanStack Form 采用的是 “发布-订阅”(Pub/Sub) 模式的响应式。
form.Field 或 form.Subscribe 时,实际上是在告诉商店:“我对 firstName 这个字段感兴趣,它变了请通知我。”参考文档:https://tanstack.com/form/latest/docs/framework/react/guides/reactivity
The useStore hook is perfect when you need to access form values within the logic of your component.
如果是在逻辑代码里面需要访问values,那么就使用useState。

可以看到,两个变量都有响应式了。

<form.Subscribe />The form.Subscribe component is best suited when you need to react to something within the UI of your component. For example, showing or hiding ui based on the value of a form field.
如果是在UI代码里面需要访问values,就使用<form.Subscribe />。

可以看到,如果想在UI里面订阅响应式,那么就使用<form.Subscribe />。这个例子就是订阅了firstName,只有firstName有值时,才会显示lastName输入框。

在 @tanstack/react-form(以及更底层的 @tanstack/form-core)中,Listeners(监听器) 是一种允许你在表单状态发生特定变化时,执行副作用(Side Effects)的机制。
如果说 Reactivity(响应式) 是为了更新 UI,那么 Listeners 就是为了触发 逻辑。
Listeners 是附加在表单或字段上的回调函数。它们不直接参与 UI 的渲染,而是在表单的生命周期事件发生时被“通知”。
在 TanStack Form 中,你最常接触到的 Listener 实际上是各种以 on 开头的配置项,例如 onChange、onBlur、onMount 等。
当一个字段的值改变时,去修改另一个字段的值。这是 Listeners 最强大的舞台。
onChange 监听器,自动清空“城市”字段并请求该省份下的城市列表。你不想等用户点提交按钮,而是想在用户每输入一个字或离开输入框时就同步到后台。
onChange 或 onBlur 监听器中调用 API。在数据存入 Store 之前进行转换。
将表单内部的状态同步到表单外部(如 Redux、Zustand 或 URL 查询参数中)。
在 TanStack Form 中,你可以在两个层级设置监听器:
这是最常用的方式,用于处理特定字段的逻辑。
xxxxxxxxxx151<form.Field2 name="password"3 // 注意:listeners是定义在 form.Field 上面的,不要和 input 里面的onChange搞混了4 listeners: {{5 // 这是一个 onChange 监听器6 onChange: ({value}) => {7 // 逻辑:当密码改变时,清空“确认密码”字段8 form.setFieldValue('confirmPassword', '')9 return value // 必须返回处理后的值10 }11 }}12 children={(field) => (13 <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />14 )}15/>用于处理影响全局的逻辑。
xxxxxxxxxx121const form = useForm({2 defaultValues: { },3 listeners: {4 onChange: ({ formApi, fieldApi }) => {5 console.log(formApi, fieldApi);6 },7 // 全局值改变监听8 onValuesChange: (values) => {9 console.log('表单数据变了,准备缓存到 localStorage', values)10 }11 },12})很多初学者会混淆这两者,它们的本质区别在于:你是想改变“长相”还是想执行“动作”?
| 维度 | Reactivity (响应式) | Listeners (监听器) |
|---|---|---|
| 关注点 | UI 状态 | 行为逻辑 |
| 触发结果 | 重新渲染组件 (Re-render) | 执行一段代码 (Function Call) |
| 典型场景 | 显示错误文字、禁用按钮 | 联动修改其他字段、发送埋点、API 请求 |
| 实现方式 | field.state.value / useStore | onChange / onBlur / onMount |
在<form.Subscribe />例子中,其实有一个问题,就是firstName没值时,lastName虽然可以隐藏,但是lastName里面的值并没有清空:

此时可以定义listeners。

可以看到,lastName输入框隐藏之后,值也被清空了。

同样,可以定义全局的listeners:
xxxxxxxxxx121const form = useForm({2 formOpts,3 onSubmit: (data) => {4 console.log({ data });5 },6 // 全局 listeners7 listeners: {8 onChange: ({ formApi, fieldApi }) => {9 console.log(formApi, fieldApi);10 },11 },12 });将对象里面的变量绑定到form.Field上面,使用点路径(Dot Notation)访问。其余的部分与string、number、boolean这些一致。
xxxxxxxxxx161<form.Field name="address.street">2 {(field) => {3 return (4 <div className="mb-4">5 <input6 type="text"7 placeholder="street"8 className="border border-gray-600 p-2 w-full"9 value={field.state.value}10 onChange={(e) => field.handleChange(e.target.value)}11 onBlur={field.handleBlur}12 />13 </div>14 );15 }}16</form.Field>可以看到,其实form.Field里面的代码有很多相似的地方,重点就是name属性。
基本使用方法是,在form.Field的name属性上,绑定数组的变量名。然后指定mode="array",接着使用field.state.value.map来渲染数组。
<form.Field name="people" mode="array"></form.Field>
xxxxxxxxxx191function App() {2 const form = useForm({3 defaultValues: {4 people: [],5 },6 })78 return (9 <form.Field name="people" mode="array">10 {(field) => (11 <div>12 {field.state.value.map((_, i) => {13 // ...14 })}15 </div>16 )}17 </form.Field>18 )19}然后使用field.pushValue、field.removeValue来新增或者删除数组里面的元素。
数组里面的对象,应该怎么渲染呢?如果有交互,还是使用form.Field来渲染,注意要指定key属性,并且name的值是name={people[${i}].name}这样写,用到了数组的索引。
xxxxxxxxxx81<form.Field key={i} name={`people[${i}].name`}>2 {(subField) => (3 <input4 value={subField.state.value}5 onChange={(e) => subField.handleChange(e.target.value)}6 />7 )}8</form.Field>xxxxxxxxxx551<form.Field name="skills" mode="array">2 {(field) => (3 <>4 <h3>Skills</h3>5 {field.state.value.map((_, index) => (6 <div className="flex gap-2 items-center mb-2">7 <form.Field key={index} name={`skills[${index}].name`}>8 {(subField) => (9 <input10 type="text"11 className="border border-gray-500 p-2 w-1/2"12 placeholder="enter name"13 value={subField.state.value}14 onChange={(e) => subField.handleChange(e.target.value)}15 onBlur={subField.handleBlur}16 />17 )}18 </form.Field>19 <form.Field key={index} name={`skills[${index}].level`}>20 {(subField) => (21 <select22 className="border border-gray-500 p-2 w-1/2"23 value={subField.state.value}24 onChange={(e) =>25 subField.handleChange(e.target.value as any)26 }27 onBlur={subField.handleBlur}>28 <option value="beginner">Beginner</option>29 <option value="intermediate">Intermediate</option>30 <option value="expert">Expert</option>31 </select>32 )}33 </form.Field>34 <button35 className="bg-red-400 text-white p-1 ms-2"36 onClick={() => field.removeValue(index)}>37 del38 </button>39 </div>40 ))}41 <button42 type="button"43 className="mb-2"44 onClick={() =>45 field.pushValue({46 id: nanoid(),47 name: "",48 level: "beginner",49 })50 }>51 Add Skill52 </button>53 </>54 )}55</form.Field>结构还是很简单的,就是可读性不好,嵌套很多。结合UI看就懂了。可以看到,功能正常。

Field Validation(字段校验)
TanStack Form 允许你精准控制校验在什么时候发生。主要有三个核心时机:
onChange:用户每输入一个字符就校验一次(实时反馈)。onBlur:用户离开输入框时校验(避免用户正在输入时频繁报错)。onSubmit:点击提交按钮时进行的最终检查。你可以直接在 form.Field 的 validators 属性中定义这些逻辑:
xxxxxxxxxx221<form.Field2 name="age"3 validators={{4 onChange: ({ value }) => 5 value < 18 ? '你必须年满 18 岁' : undefined,6 onBlur: ({ value }) => 7 value > 100 ? '年龄似乎不太真实' : undefined,8 }}9 children={(field) => (10 <div>11 <input 12 value={field.state.value} 13 onChange={(e) => field.handleChange(Number(e.target.value))}14 onBlur={field.handleBlur}15 />16 {/* 显示校验结果 */}17 {!field.state.meta.isValid && (18 field.state.meta.errors.map(err => <p key={err} style={{color: 'red'}}>{err}</p>19 ))}20 </div>21 )}22/>这是处理“检查用户名是否已存在”等场景的神器。你可以将校验函数标记为 async,TanStack Form 会自动处理挂起状态。
asyncDebounceMs:非常重要!设置防抖时间,防止用户每打一个字就发一次 API 请求。xxxxxxxxxx181<form.Field2 name="username"3 asyncDebounceMs={500}4 validators={{5 onChangeAsync: async ({ value }) => {6 const isTaken = await checkAvailability(value)7 return isTaken ? '用户名已被占用' : undefined8 },9 }}10 children={(field) => (11 <div>12 <input {} />13 {/* 配合 meta 数据显示加载状态 */}14 {field.state.meta.isValidating && <span>检查中...</span>}15 {field.state.meta.errors && <span>{field.state.meta.errors}</span>}16 </div>17 )}18/>手写校验逻辑很累且容易出错。TanStack Form 提供了 Standard Schema Adapter,让你能直接使用 Zod 这种强大的库。
xxxxxxxxxx141import { z } from 'zod'23const userSchema = z.string().min(3, '至少3个字符').email('格式不正确')45// 在 Field 中使用6<form.Field7 name="email"8 validators={{9 onChange: userSchema, // 直接传入 Zod Schema10 }}11 children={(field) => (12 // ... 自动享受 Zod 的错误提示13 )}14/>当校验发生时,field.state.meta 会发生变化,你可以利用这些状态提升体验:
| 状态属性 | 作用 |
|---|---|
errors | 存储当前所有的错误信息数组。 |
errorMap | 键值对形式的错误,方便针对特定事件(如 onBlur)取值。 |
isValidating | 布尔值,表示异步校验是否正在运行。 |
isValid | 该字段当前是否合法。 |
有时候 A 字段的合法性取决于 B 字段(比如“确认密码”必须等于“密码”)。
xxxxxxxxxx121<form.Field2 name="confirmPassword"3 validators={{4 onChangeListenTo: ['password'], // 监听 password 字段的变化5 onChange: ({ value, fieldApi }) => {6 if (value !== fieldApi.form.getFieldValue('password')) {7 return '两次密码输入不一致'8 }9 },10 }}11 children={}12/>onChange / onBlur:处理简单的逻辑。onChangeAsync + asyncDebounceMs:处理 API 校验。Zod:保持代码整洁和强类型。meta:利用 isValidating 和 isTouched 优化用户体验。
可以看到,当firstName长度<=2时,就会报错。当然也可以设置为onBlur或者onSubmit时校验,看需求。

在 TanStack Form 中,Form Validation(表单级校验) 与字段级校验相辅相成。如果说字段级校验关注的是“局部细节”(如邮箱格式是否正确),那么表单级校验关注的是“整体合规性”。
它通常用于处理跨字段逻辑(如两次密码一致性)以及提交前的最终关卡。
你可以在 useForm 的配置中直接定义表单级的校验逻辑:
onValuesChange: 只要表单中任何值发生变化,就会触发校验。onValuesChangeAsync: 针对表单全体数据的异步校验(配合防抖使用)。onSubmit: 提交时的最后一次同步/异步校验。最典型的场景是“密码”与“确认密码”的匹配。虽然可以写在字段级,但在表单级处理逻辑会更清晰。
xxxxxxxxxx151const form = useForm({2 defaultValues: {3 password: '',4 confirmPassword: '',5 },6 // 表单级同步校验7 validators: {8 onChange: ({ value }) => {9 if (value.password !== value.confirmPassword) {10 return '两次输入的密码不一致'11 }12 return undefined13 },14 },15})表单级校验产生的错误会存储在 form.state.meta.errors 中,所以在field里面是不能直接获取到的,那么需要使用useStore来订阅errors。
xxxxxxxxxx51const errors = useStore(form.store, (state) => state.errors);23{errors && (4 <p className="text-red-500 text-start mb-4">{errors.join(", ")}</p>5)}
表单级校验产生的错误会存储在 form.state.meta.errors 中。你可以将其显示在表单的顶部或底部。
xxxxxxxxxx231function MyForm() {2 const form = useForm({ })34 return (5 <form onSubmit={}>6 {/* 订阅并显示表单级错误 */}7 <form.Subscribe8 selector={(state) => state.meta.errors}9 children={(errors) => (10 errors.length > 0 && (11 <div className="global-error-banner">12 {errors.map(err => <p key={err}>{err}</p>)}13 </div>14 )15 )}16 />17 18 {/* 字段... */}19 20 <button type="submit">注册</button>21 </form>22 )23}这是目前最推荐的做法。通过定义一个完整的 Zod Schema,你可以一次性完成字段级和表单级的所有逻辑。
xxxxxxxxxx161import { z } from 'zod'23const signUpSchema = z.object({4 password: z.string().min(8),5 confirmPassword: z.string()6}).refine((data) => data.password === data.confirmPassword, {7 message: "密码不匹配",8 path: ["confirmPassword"], // 错误信息可以指向特定字段9})1011const form = useForm({12 defaultValues: { password: '', confirmPassword: '' },13 validators: {14 onChange: signUpSchema, // 这里的全表校验会自动分发错误15 },16})| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 基础格式 (如 Email, 必填) | Field Validation | 性能更好,报错直接关联到输入框,用户体验更直观。 |
| 逻辑依赖 (如 A 依赖 B) | Form Validation | 在一个地方访问所有数据 value 更容易编写比较逻辑。 |
| 业务一致性 (如日期区间) | Form Validation | 方便处理涉及多个字段的联合错误。 |
表单级校验会直接影响 form.state.meta.canSubmit 属性:
canSubmit: falsecanSubmit: falseisValidating) canSubmit: false这就是为什么你只需要订阅 canSubmit 就能完美控制提交按钮的禁用状态。
使用form.Subscribe订阅canSubmit和isSubmitting状态,做相应的逻辑、样式。
xxxxxxxxxx151const form = useForm(/* ... */)23return (4 /* ... */56 // Dynamic submit button7 <form.Subscribe8 selector={(state) => [state.canSubmit, state.isSubmitting]}9 children={([canSubmit, isSubmitting]) => (10 <button type="submit" disabled={!canSubmit}>11 {isSubmitting ? '...' : 'Submit'}12 </button>13 )}14 />15)xxxxxxxxxx241const form = useForm({2 formOpts,3 onSubmit: (data) => {4 console.log({ data });5 },6 validators: {7 onChange: ({ value }) => {8 if (value.firstName !== value.lastName) {9 return "firstName must be equal to lastName";10 }11 },12 },13 listeners: {14 onChange: ({ formApi, fieldApi }) => {15 console.log(formApi, fieldApi);16 },17 },18 });1920const errors = useStore(form.store, (state) => state.errors);2122{errors && (23 <p className="text-red-500 text-start mb-4">{errors.join(", ")}</p>24)}可以看到,全局errors的信息显示成功了。

文档讲解的很清楚了。可以直接参考:https://tanstack.com/form/latest/docs/framework/react/guides/validation#asynchronous-functional-validation。
在表单开发中,异步校验(Async Validation) 是处理“需要服务器验证”逻辑的核心,比如检查用户名是否被占用、验证优惠券代码或实时校验银行卡号。
TanStack Form 对异步校验的处理非常优雅,它内置了防抖(Debounce)、竞态检查和挂起状态(Pending States)的管理。
onChangeAsync 与 asyncDebounceMs要实现异步校验,你主要会用到这两个属性:
onChangeAsync: 接受一个异步函数,返回错误信息(字符串)或 undefined。asyncDebounceMs: 关键参数。它规定了用户停止输入多久后才触发异步校验。如果没有它,用户每打一个字都会向后端发送一次 API 请求,这会造成服务器压力并导致性能卡顿。以下是一个模拟“检查用户名是否重复”的典型例子:
xxxxxxxxxx331<form.Field2 name="username"3 // 停顿 500ms 后再发请求4 asyncDebounceMs={500}5 validators={{6 onChangeAsync: async ({ value }) => {7 // 模拟 API 调用8 await new Promise((resolve) => setTimeout(resolve, 1000))9 10 const takenNames = ['admin', 'gemini', 'tanstack']11 if (takenNames.includes(value.toLowerCase())) {12 return '该用户名已被占用'13 }14 return undefined15 },16 }}17 children={(field) => (18 <div>19 <input20 value={field.state.value}21 onChange={(e) => field.handleChange(e.target.value)}22 />23 24 {/* 状态展示:正在校验时显示 Loading */}25 {field.state.meta.isValidating && <p>正在检查用户名可用性...</p>}26 27 {/* 显示错误信息 */}28 {field.state.meta.errors && (29 <p style={{ color: 'red' }}>{field.state.meta.errors}</p>30 )}31 </div>32 )}33/>当异步校验开始时,field.state.meta 会发生以下变化:
开始校验:isValidating 变为 true。
正在进行:此时 canSubmit 会自动变为 false(防止用户在校验完成前提交)。
结果返回:
errors 数组被填充,isValid 变为 false。errors 为空,isValid 变为 true。结束校验:isValidating 回到 false。
在原生开发或简单的 React 开发中,异步校验常遇到竞态问题(Race Conditions):
比如用户先输了 "abc"(请求 A 慢),接着输了 "abcd"(请求 B 快)。如果 A 晚于 B 返回,界面可能会显示 A 的旧报错。
TanStack Form 的优势:
lodash.debounce 的库。除了 onChangeAsync,你还可以在表单级设置 onSubmitAsync。 即使前端所有校验都通过了,在点击提交瞬间,你可以再次发起一次全局的异步检查:
xxxxxxxxxx91const form = useForm({2 onSubmitAsync: async ({ value }) => {3 // 提交前的最后一次服务器端预检4 const result = await finalServerCheck(value)5 if (!result.ok) {6 // 处理错误7 }8 }9})
asyncDebounceMs属性,会让每一个async请求防抖,validators里面的所有async请求。
也可以单独设置validators里面的防抖属性:

如果设置了onChangeAsyncDebounceMs,那么会覆盖asyncDebounceMs。
validators 中先写 onChange(同步),再写 onChangeAsync。只有同步校验(如非空判断、长度判断)通过后,才会触发异步校验,这样可以节省大量的网络请求。isValidating 显示一个微小的 Spinner 或提示文本,否则用户会觉得输入框“没反应”。xxxxxxxxxx441<form.Field2 name="firstName"3 validators={{4 onChangeAsyncDebounceMs: 500,5 onChangeAsync: async ({ value }) => {6 await fetch("https://jsonplaceholder.typicode.com/todos/1");7 await new Promise((r) => setTimeout(r, 1000));8 if (value !== "John") {9 return "Your name must be John.";10 }11 },12 onBlur: ({ value }) => {13 if (value.length <= 2) {14 return "First name must be at least 2 letters";15 }16 },17 }}18 listeners={{19 onChange: ({ value }) => {20 if (!value) {21 form.setFieldValue("lastName", "");22 }23 },24 }}>25 {(field) => {26 return (27 <div className="mb-4">28 <input29 type="text"30 placeholder="First Name"31 className="border border-gray-600 p-2 w-full"32 value={field.state.value}33 onChange={(e) => field.handleChange(e.target.value)}34 onBlur={field.handleBlur}35 />36 {!field.state.meta.isValid && (37 <div className="text-red-500 mt-2">38 {field.state.meta.errors.join(", ")}39 </div>40 )}41 </div>42 );43 }}44</form.Field>可以看到,输入时触发了async校验。而且因为有防抖,所以校验不会很频繁。

参考文档:https://tanstack.com/form/latest/docs/framework/react/guides/dynamic-validation
它主要解决的问题是: 在表单提交前(或第一次提交后)可以“延迟/改变”校验规则,最经典的场景就是:
这个功能是 TanStack Form 独有的设计,其他表单库(如 React Hook Form)通常是通过 reValidateMode 或手动控制来近似实现,而 TanStack Form 通过 onDynamic + revalidateLogic 提供了原生支持。
必须搭配 validationLogic: revalidateLogic() 使用
如果不加这个,onDynamic 永远不会被调用。
onDynamic 校验函数
revalidateLogic 的配置(控制校验时机)
xxxxxxxxxx41validationLogic: revalidateLogic({2 mode: 'submit', // 第一次提交前用这个模式(通常宽松)3 modeAfterSubmission: 'blur', // 提交后改成这个模式(通常严格)4})常见组合:
mode: 'submit' → modeAfterSubmission: 'change' / 'blur'mode: 'change' → modeAfterSubmission: 'blur'xxxxxxxxxx361import { useForm, revalidateLogic } from '@tanstack/react-form'23const form = useForm({4 defaultValues: {5 firstName: '',6 lastName: '',7 },8 9 // 必须加这个才能启用 onDynamic10 validationLogic: revalidateLogic({11 mode: 'submit', // 提交前几乎不校验(或只校验必要字段)12 modeAfterSubmission: 'blur', // 提交后每个字段 blur 时都校验13 }),1415 validators: {16 // onDynamic 会在 revalidateLogic 指定的时机触发17 onDynamic: ({ value }) => {18 const errors: Record<string, string> = {}19 20 if (!value.firstName) {21 errors.firstName = 'First name is required after submission'22 }23 if (!value.lastName) {24 errors.lastName = 'Last name is required after submission'25 }26 27 return Object.keys(errors).length ? errors : undefined28 },29 30 // 也可以混用普通校验(提交前一直生效)31 onChange: ({ value }) => {32 // 可以留空,或只做最基本的校验33 return undefined34 }35 }36})xxxxxxxxxx221<form.Field2 name="age"3 validators={{4 onDynamic: ({ value }) => 5 value >= 18 ? undefined : 'Must be 18+ after submission',6 }}7 children={(field) => (8 <>9 <input10 type="number"11 value={field.state.value ?? ''}12 onChange={(e) => field.handleChange(e.target.valueAsNumber)}13 onBlur={field.handleBlur}14 />15 {field.state.meta.errorMap.onDynamic && (16 <span style={{ color: 'red' }}>17 {field.state.meta.errorMap.onDynamic}18 </span>19 )}20 </>21 )}22/>xxxxxxxxxx121validators: {2 onDynamicAsyncDebounceMs: 800,3 onDynamicAsync: async ({ value }) => {4 if (!value.username) return { username: 'Required' }5 6 // 模拟 API 检查7 await new Promise(r => setTimeout(r, 600))8 return value.username === 'admin' 9 ? { username: 'Username already taken' } 10 : undefined11 }12}xxxxxxxxxx71// form 级别错误2{form.state.errorMap.onDynamic?.firstName && (3 <div>{form.state.errorMap.onDynamic.firstName}</div>4)}56// field 级别7{field.state.meta.errorMap.onDynamic}xxxxxxxxxx451const form = useForm({2 formOpts,3 onSubmit: (data) => {4 console.log({ data });5 },6 // 定义validationLogic,否则dynamic validation不生效7 validationLogic: revalidateLogic({8 mode: "submit",9 modeAfterSubmission: "blur",10 }),11 validators: {12 onChange: ({ value }) => {13 14 },15 },16 listeners: {17 onChange: ({ formApi, fieldApi }) => {18 console.log(formApi, fieldApi);19 },20 },21 });2223 <form.Field24 name="firstName"25 validators={{26 // 定义dynamic校验27 onDynamic: ({ value }) => {28 if (value !== "John") {29 return "Your name must be John.";30 }31 },32 }}33 listeners={{34 onChange: ({ value }) => {35 if (!value) {36 form.setFieldValue("lastName", "");37 }38 },39 }}>40 {(field) => {41 return (42 。。。。。。43 );44 }}45 </form.Field>可以看到,第一次提交表单的时候,触发submit校验,后续的会触发blur校验。

Linked Fields主要是如何让两个(或多个)表单字段之间建立联动关系,确保当其中一个字段变化时,另一个字段的校验逻辑能够及时重新执行,从而保持数据一致性和实时校验准确性。
这是 TanStack Form 处理字段间相互依赖场景的核心技巧,尤其适合密码确认、起始/结束日期比较、地址联动等常见需求。
当两个字段相互依赖时(例如 password 和 confirm_password),如果用户先改了 confirm_password,再改了 password,普通的校验方式会导致问题:
confirm_password 的校验只在它自己变化时触发password 变化后,confirm_password 的错误状态可能不会更新(显示过时的错误或不显示应有的错误)解决方案:使用 onChangeListenTo / onBlurListenTo在依赖字段(被影响的那个)的 validators 配置中,添加监听:
xxxxxxxxxx121validators={{2 onChangeListenTo: ['password'], // 当 password 变化时,重新执行本字段的 onChange 校验3 // 或 onBlurListenTo: ['password'], // 当 password 失去焦点时才重新校验(更克制)4 onChange: ({ value, fieldApi }) => {5 const password = fieldApi.form.getFieldValue('password')6 7 if (value !== password) {8 return '两次输入的密码不一致'9 }10 return undefined11 }12}}关键点:
onChangeListenTo: 数组,列出要监听的字段名(字符串)onChange 校验函数fieldApi.form.getFieldValue('xxx') 获取其他字段的最新值xxxxxxxxxx481const form = useForm({2 defaultValues: {3 password: '',4 confirm_password: ''5 }6})78return (9 <>10 {/* 普通密码字段 */}11 <form.Field name="password">12 {(field) => (13 <div>14 <label>密码</label>15 <input16 value={field.state.value ?? ''}17 onChange={(e) => field.handleChange(e.target.value)}18 />19 </div>20 )}21 </form.Field>2223 {/* 依赖密码的确认字段 */}24 <form.Field25 name="confirm_password"26 validators={{27 onChangeListenTo: ['password'], // ← 核心在这里28 onChange: ({ value, fieldApi }) => {29 const pw = fieldApi.form.getFieldValue('password')30 return value === pw ? undefined : '密码不匹配'31 }32 }}33 >34 {(field) => (35 <div>36 <label>确认密码</label>37 <input38 value={field.state.value ?? ''}39 onChange={(e) => field.handleChange(e.target.value)}40 />41 {field.state.meta.errors?.map(err => (42 <div key={err} style={{ color: 'red' }}>{err}</div>43 ))}44 </div>45 )}46 </form.Field>47 </>48)| 配置项 | 作用 | 使用时机建议 |
|---|---|---|
onChangeListenTo | 监听字段每次变化时触发校验 | 实时反馈强(推荐大多数场景) |
onBlurListenTo | 只在监听字段失去焦点时触发校验 | 减少不必要的校验,性能更好 |
fieldApi.form.getFieldValue(fieldName) | 在校验函数中安全获取其他字段最新值 | 几乎所有联动场景必用 |
xxxxxxxxxx471<form.Field name="password">2 {(field) => {3 return (4 <div className="mb-4">5 <input6 type="password"7 placeholder="enter password"8 className="border border-gray-600 p-2 w-full"9 value={field.state.value}10 onChange={(e) => field.handleChange(e.target.value)}11 onBlur={field.handleBlur}12 />13 </div>14 );15 }}16</form.Field>17<form.Field18 name="confirmPassword"19 validators={{20 onChangeListenTo: ["password"],21 onChange: ({ value, fieldApi }) => {22 if (value !== fieldApi.form.getFieldValue("password")) {23 return "Passwords do not match";24 }25 return undefined;26 },27 }}>28 {(field) => {29 return (30 <div className="mb-4">31 <input32 type="password"33 placeholder="enter confirmPassword"34 className="border border-gray-600 p-2 w-full"35 value={field.state.value}36 onChange={(e) => field.handleChange(e.target.value)}37 onBlur={field.handleBlur}38 />39 {!field.state.meta.isValid && (40 <div className="text-red-500 mt-2">41 {field.state.meta.errors.join(", ")}42 </div>43 )}44 </div>45 );46 }}47</form.Field>可以看到,当password改变的时候,confirmPassword里面会触发校验。

在 TanStack Form 中使用外部 schema 校验库(如 Zod、Yup、Valibot、ArkType 等)来定义表单的整体校验规则,而不是手动写一堆 onChange / onBlur 函数。
核心思想:把校验逻辑集中到 schema 定义里,让 TanStack Form 自动解析 schema 并应用到字段级别和表单级别。
TanStack Form 提供了开箱即用的适配器,目前支持以下库:
| Schema 库 | 适配器包名 | 导入方式 | 成熟度/推荐度 |
|---|---|---|---|
| Zod | @tanstack/zod-form-adapter | import { zodValidator } from '@tanstack/zod-form-adapter' | ★★★★★(最推荐) |
| Yup | @tanstack/yup-form-adapter | yupValidator | ★★★★ |
| Valibot | @tanstack/valibot-form-adapter | valibotValidator | ★★★★(轻量) |
| ArkType | @tanstack/arktype-form-adapter | arktypeValidator | ★★★ |
| Custom | 自己实现 | 自定义 validatorAdapter | 灵活但麻烦 |
Zod 是目前最受欢迎的选择,因为类型推导最强、生态最好。
formSchema定义之后,就不需要写ts类型了,只需要使用z.infer<typeof formSchema>,就可以推导出类型。非常方便。
xxxxxxxxxx341import { useForm } from '@tanstack/react-form'2import { zodValidator } from '@tanstack/zod-form-adapter'3import { z } from 'zod'45// 1. 定义 schema(推荐放在单独文件)6const formSchema = z.object({7 firstName: z.string().min(2, '至少 2 个字符'),8 email: z.string().email('请输入有效的邮箱'),9 age: z.number().min(18, '必须年满 18 岁').max(100),10 // 嵌套对象、数组、refine 等 Zod 所有功能都支持11 address: z.object({12 street: z.string().min(1),13 city: z.string(),14 }),15})1617// 2. 在 useForm 中使用18const form = useForm({19 defaultValues: {20 firstName: '',21 email: '',22 age: 0,23 address: { street: '', city: '' },24 },25 26 // 核心:传入 validatorAdapter + schema27 validatorAdapter: zodValidator(),28 29 validators: {30 onChange: formSchema, // 整个表单的 onChange 校验31 // 也可以只对部分模式用 schema32 // onChange: formSchema.partial() // 宽松模式33 },34})xxxxxxxxxx31validators: {2onChange: formSchema, // 整个表单的 onChange 校验3},将整个form都绑定到onChange上面,当一个field校验不通过时,会自动校验其余的field,但很可能其余的field都没有开始交互,这样用户体验不好。
可以这样显示错误信息,添加一个
field.state.meta.isDirty判断:xxxxxxxxxx71{!field.state.meta.isValid && field.state.meta.isDirty && (2<div className="text-red-500 mt-2">3{field.state.meta.errors4.map((error) => error?.message)5.join(", ")}6</div>7)}
xxxxxxxxxx211<form.Field2 name="email"3 validators={{4 onChange: formSchema.shape.email, // 只用 email 这部分的 schema5 // 或 onBlur: formSchema.shape.email6 }}7>8 {(field) => (9 <div>10 <input11 value={field.state.value ?? ''}12 onChange={(e) => field.handleChange(e.target.value)}13 />14 {field.state.meta.errorMap.onChange && (15 <span style={{ color: 'red' }}>16 {field.state.meta.errorMap.onChange}17 </span>18 )}19 </div>20 )}21</form.Field>refineAsync,TanStack Form 也支持 onChangeAsync / onBlurAsyncformSchema.partial()、formSchema.pick({ email: true })、formSchema.omit({ password: true }).refine()、.superRefine() 可以实现跨字段校验field.state.meta.errorMap(onChange、onBlur、onSubmit 等)| 场景 | 推荐做法 |
|---|---|
| 简单表单 | 字段级 onChange: schema.shape.xxx |
| 复杂表单 + 跨字段校验 | form 级别 onChange: schema + refine |
| 提交前宽松 / 提交后严格 | 配合 revalidateLogic + onDynamic 使用 |
| 需要类型推导 | 优先 Zod(配合 z.infer<typeof formSchema>) |
xxxxxxxxxx401<form.Field2 name="firstName"3 validators={{4 onBlur: z.string().refine((val) => val !== "John", {5 error: "First name can not be John",6 }),7 onChange: z8 .string()9 .min(2, "First name must be at least 2 characters")10 .max(50),11 }}12 listeners={{13 onChange: ({ value }) => {14 if (!value) {15 form.setFieldValue("lastName", "");16 }17 },18 }}>19 {(field) => {20 return (21 <div className="mb-4">22 <input23 type="text"24 placeholder="First Name"25 className="border border-gray-600 p-2 w-full"26 value={field.state.value}27 onChange={(e) => field.handleChange(e.target.value)}28 onBlur={field.handleBlur}29 />30 {!field.state.meta.isValid && (31 <div>32 {field.state.meta.errors.map((error) => (33 <div className="text-red-500 mt-2">{error?.message}</div>34 ))}35 </div>36 )}37 </div>38 );39 }}40</form.Field>可以看到,校验都触发了,而且写规则很简单。

在 Zod 中使用 refine 和 refineAsync 是处理复杂/跨字段/异步校验的最常用方式,尤其在 TanStack Form 中结合使用时非常强大。
下面是目前(2025 年底)最实用、最常见的几种用法和写法,涵盖同步 refine、异步 refine 和在 TanStack Form 中的集成方式。
xxxxxxxxxx121import { z } from "zod";23const formSchema = z.object({4 password: z.string().min(8, "至少 8 个字符"),5 confirmPassword: z.string(),6}).refine(7 (data) => data.password === data.confirmPassword,8 {9 message: "两次输入的密码不一致",10 path: ["confirmPassword"], // 错误显示在 confirmPassword 字段上11 }12);关键点:
xxxxxxxxxx171const formSchema = z.object({2 startDate: z.date(),3 endDate: z.date(),4 participants: z.number().min(1),5}).refine(6 (data) => data.startDate < data.endDate,7 { message: "结束日期不能早于开始日期", path: ["endDate"] }8).refine(9 (data) => data.participants > 0,10 { message: "至少需要 1 名参与者", path: ["participants"] }11).refine(12 (data) => {13 const diffDays = (data.endDate.getTime() - data.startDate.getTime()) / (1000 * 3600 * 24);14 return diffDays <= 90;15 },16 { message: "活动时长不能超过 90 天", path: ["endDate"] }17);最常见场景:检查用户名/邮箱是否已被占用、验证码是否正确等。
xxxxxxxxxx271const formSchema = z.object({2 username: z.string().min(3),3 email: z.string().email(),4}).refineAsync(5 async (data) => {6 // 模拟 API 调用7 const response = await fetch(`/api/check-username?username=${data.username}`);8 const result = await response.json();9 return result.available; // true = 可用10 },11 {12 message: "用户名已被占用",13 path: ["username"],14 }15).refineAsync(16 async (data) => {17 const res = await fetch(`/api/check-email`, {18 method: "POST",19 body: JSON.stringify({ email: data.email }),20 });21 return res.ok;22 },23 {24 message: "邮箱验证失败或已被注册",25 path: ["email"],26 }27);重要注意:
refineAsync 是异步的,需要在 TanStack Form 中搭配 onChangeAsync 或 onSubmitAsync 使用refineAsync 会并行执行(性能更好)注意:如果涉及到linked field校验,那么refine写在formSchema上比较好,如果是单个的refine校验,也可以写在具体的field标签上。
xxxxxxxxxx541import { useForm } from "@tanstack/react-form";2import { zodValidator } from "@tanstack/zod-form-adapter";3import { z } from "zod";45const formSchema = z.object({6 username: z.string().min(3),7 email: z.string().email(),8 password: z.string().min(8),9 confirmPassword: z.string(),10}).refine(11 (data) => data.password === data.confirmPassword,12 { message: "密码不一致", path: ["confirmPassword"] }13).refineAsync(14 async (data) => {15 // 模拟 1.5s 延迟的 API 检查16 await new Promise(r => setTimeout(r, 1500));17 return data.username !== "admin"; // 假装 admin 被占用18 },19 { message: "此用户名已被占用", path: ["username"] }20);2122function MyForm() {23 const form = useForm({24 defaultValues: {25 username: "",26 email: "",27 password: "",28 confirmPassword: "",29 },30 31 validatorAdapter: zodValidator(),32 33 validators: {34 // 提交时进行完整校验(包括异步)35 onSubmit: formSchema,36 37 // 可选:实时校验基础规则38 onChange: formSchema.partial(), // 部分模式,宽松一些39 },40 });4142 return (43 <form44 onSubmit={(e) => {45 e.preventDefault();46 e.stopPropagation();47 form.handleSubmit();48 }}49 >50 {/* 字段省略... */}51 <button type="submit">提交</button>52 </form>53 );54}快速对比表:refine vs refineAsync vs superRefine
| 方法 | 同步/异步 | 能拿到什么 | 适合场景 | 性能影响 |
|---|---|---|---|---|
.refine | 同步 | 整个表单数据 | 简单跨字段逻辑 | 极低 |
.refineAsync | 异步 | 整个表单数据 | API 检查、数据库唯一性等 | 中等 |
.superRefine | 同步 | ctx + 整个数据 | 非常复杂的自定义逻辑(带 ctx.addIssue) | 低 |
z.object({...}).shape.xxxrefinerefineAsyncsuperRefine + ctx.addIssuerevalidateLogic + onDynamic作用:在校验通过后,对数据进行任意转换,常用于:
重要特性:
案例:后端只需要数组的id,而不是整个对象数组。
xxxxxxxxxx291import { z } from 'zod';23// 1. 先定义单个技能的 Schema4const skillSchema = z.object({5 name: z.string().min(1, '技能名称不能为空'),6 id: z.string(), // 如果是自动生成的 ID,可以只设为 string7 level: z.enum(['beginner', 'intermediate', 'expert'], {8 errorMap: () => ({ message: '请选择有效的熟练度' }),9 }),10});1112// 2. 定义整个 skills 数组的 Schema13const skillsSchema = z14 .array(skillSchema)15 .min(1, '请至少添加一个技能') // 校验数组长度16 .max(10, '技能数量不能超过10个');1718// 定义完整的 formSchema19const registerFormSchema = z.object({20 firstName: z.string().min(2),21 // ... 其他字段22 skills: skillsSchema, // 引用上面定义的数组 Schema23 acceptTerms: z.boolean().refine((val) => val === true, '必须接受条款'),24}).transform(data => {25 return {26 data,27 skills: data.skills.map(s => s.id)28 }29})作用:启动校验、拿到安全数据。
xxxxxxxxxx91const form = useForm({2 formOpts,3 onSubmit: (data) => {4 // 可以输出看一下,二者的区别5 console.log({ data });6 console.log(formSchema.data)7 },8 9});在 TanStack Form(@tanstack/react-form)中处理一个表单里有多个 submit 按钮的场景是非常常见的,比如:
核心问题是:如何区分用户点击的是哪个 submit 按钮,并在提交时知道具体意图?推荐的几种主流处理方式
| 方式 | 实现难度 | 代码清晰度 | 推荐场景 | 备注 |
|---|---|---|---|---|
| 1. 隐藏的 intent 字段 + value | ★☆☆ | ★★★★★ | 最推荐,几乎所有情况都适用 | 官方文档推荐,简单可靠 |
| 2. form.handleSubmit + 自定义 onSubmit | ★★☆ | ★★★★ | 需要更灵活控制 | 适合复杂逻辑 |
这是 TanStack Form 社区和官方最推荐的做法,原理简单:用一个隐藏字段记录当前点击的按钮意图。
xxxxxxxxxx781import { useForm } from '@tanstack/react-form'23function MyForm() {4 const form = useForm({5 defaultValues: {6 title: '',7 content: '',8 // 关键:添加一个 intent 字段,用于区分意图9 intent: 'save' as 'save' | 'publish' | 'preview',10 },11 onSubmit: async ({ value }) => {12 // 根据 intent 分支处理13 if (value.intent === 'save') {14 console.log('保存草稿', value.title, value.content)15 // api.saveDraft(value)16 } else if (value.intent === 'publish') {17 console.log('正式发布', value)18 // api.publish(value)19 } else if (value.intent === 'preview') {20 console.log('生成预览', value)21 // api.preview(value)22 }23 },24 })2526 return (27 <form28 onSubmit={(e) => {29 e.preventDefault()30 e.stopPropagation()31 form.handleSubmit()32 }}33 >34 <form.Field name="title">35 {(field) => (36 <div>37 <label>标题</label>38 <input39 value={field.state.value ?? ''}40 onChange={(e) => field.handleChange(e.target.value)}41 />42 </div>43 )}44 </form.Field>4546 {/* 隐藏的 intent 字段 */}47 <form.Field name="intent">48 {(field) => (49 <input type="hidden" value={field.state.value} name={field.name} />50 )}51 </form.Field>5253 {/* 多个 submit 按钮 */}54 <div style={{ marginTop: '1rem', display: 'flex', gap: '1rem' }}>55 <button56 type="submit"57 onClick={() => form.setFieldValue('intent', 'save')}58 >59 保存草稿60 </button>6162 <button63 type="submit"64 onClick={() => form.setFieldValue('intent', 'publish')}65 >66 发布文章67 </button>6869 <button70 type="submit"71 onClick={() => form.setFieldValue('intent', 'preview')}72 >73 预览74 </button>75 </div>76 </form>77 )78}优点:
xxxxxxxxxx91const form = useForm({2 formOpts,3 onSubmit: ({ value, meta }) => {4 console.log({ value });5 console.log({ meta });6 // 根据 meta 里面数据的不同,执行不同的逻辑7 },8 9 });xxxxxxxxxx201<div>2 <button3 type="button"4 onClick={() =>5 form.handleSubmit({6 submitAction: "save",7 })8 }>9 Submit10 </button>11 <button12 type="button"13 onClick={() =>14 form.handleSubmit({15 submitAction: "cancel",16 })17 }>18 Go Back19 </button>20</div>可以看到,输出的meta的不同。
